use anyhow::anyhow;
use chrono::{Duration, Local, NaiveDateTime};
use futures::TryFutureExt;
use reqwest::Response;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::{Arc, OnceLock};
use tokio::sync::RwLock;

use futures::future::join_all;
use sq_global::{
    WECOM_ACCESS_TOKEN_URL_CFG, WECOM_AES_KEY_CFG, WECOM_AGENT_ID_CFG, WECOM_APP_ID_CFG,
    WECOM_APP_SECRET_CFG, WECOM_CORP_ID_CFG, WECOM_MESSAGE_PUSH_URL_CFG, WECOM_TOKEN_CFG,
};

static WECOM_CLI: OnceLock<WecomClient> = OnceLock::new();
#[inline]
pub fn cli() -> &'static WecomClient {
    WECOM_CLI.get().expect("WecomClient not initialized")
}
pub async fn init() {
    let client = WecomClient::init().await;
    WECOM_CLI.set(client).expect("err initialize WecomClient")
}

#[derive(Deserialize)]
pub struct AccessTokenResp {
    errcode: i32,
    errmsg: String,
    access_token: Option<String>,
    expires_in: Option<i64>,
}

#[derive(Serialize)]
pub struct MsgPushReq<'a> {
    touser: &'a str,
    msgtype: &'a str,
    agentid: i64,
    text: HashMap<&'static str, &'a str>,
    safe: i32,
}

const PARAM_CORP_ID: &'static str = "corpid";
const PARAM_APP_SECRET: &'static str = "corpsecret";
type WecomSession = RwLock<WecomSessionInfo>;
#[derive(Default, Debug)]
pub struct WecomSessionInfo {
    acc_token: Arc<String>,
    expire_at: NaiveDateTime,
}

impl TryFrom<AccessTokenResp> for WecomSessionInfo {
    type Error = anyhow::Error;
    fn try_from(value: AccessTokenResp) -> Result<Self, Self::Error> {
        let err_code = value.errcode;
        let err_msg = value.errmsg;
        if let (Some(token), Some(expire)) = (value.access_token, value.expires_in) {
            Ok(WecomSessionInfo::new(token, expire))
        } else {
            Err(anyhow!(
                "获取access token失败！err_code:{err_code}, err_msg:{err_msg}"
            ))
        }
    }
}

impl WecomSessionInfo {
    pub fn new(acc_token: String, expire_in: i64) -> Self {
        Self {
            acc_token: Arc::new(acc_token),
            expire_at: Local::now().naive_local() + Duration::seconds(expire_in),
        }
    }
}

#[derive(Default, Debug)]
pub struct WecomClient {
    client: reqwest::Client,
    token: String,
    encoding_aes_key: String,
    app_id: String,
    corp_id: String,
    app_agent_id: i64,
    app_secret: String,
    access_token_url: String,
    message_push_url: String,
    wecom_session: WecomSession,
}

impl Deref for WecomClient {
    type Target = reqwest::Client;
    fn deref(&self) -> &Self::Target {
        &self.client
    }
}

impl WecomClient {
    pub async fn init() -> Self {
        let wecom_client = Self {
            client: sq_global::http_cli(),
            token: sq_config::get_unchecked::<String>(WECOM_TOKEN_CFG),
            encoding_aes_key: sq_config::get_unchecked::<String>(WECOM_AES_KEY_CFG),
            app_id: sq_config::get_unchecked::<String>(WECOM_APP_ID_CFG),
            corp_id: sq_config::get_unchecked::<String>(WECOM_CORP_ID_CFG),
            app_agent_id: sq_config::get_unchecked::<i64>(WECOM_AGENT_ID_CFG),
            app_secret: sq_config::get_unchecked::<String>(WECOM_APP_SECRET_CFG),
            access_token_url: sq_config::get_unchecked::<String>(WECOM_ACCESS_TOKEN_URL_CFG),
            message_push_url: sq_config::get_unchecked::<String>(WECOM_MESSAGE_PUSH_URL_CFG),
            ..Default::default()
        };
        wecom_client.init_session().await;
        wecom_client
    }
    async fn post_for_acc_token(&self) -> AccessTokenResp {
        self.get(&self.access_token_url)
            .query(&[
                (PARAM_CORP_ID, &self.corp_id),
                (PARAM_APP_SECRET, &self.app_secret),
            ])
            .send()
            .await
            .expect("请求access token失败！！！")
            .json::<AccessTokenResp>()
            .await
            .expect("反序列化access token响应失败！！！")
    }
    async fn init_session(&self) {
        let mut write_guard = self.wecom_session.write().await;
        *write_guard = self.post_for_acc_token().await.try_into().unwrap()
    }
    async fn try_init_session(&self) -> Result<WecomSessionInfo, anyhow::Error> {
        self.get(&self.access_token_url)
            .query(&[
                (PARAM_CORP_ID, &self.corp_id),
                (PARAM_APP_SECRET, &self.app_secret),
            ])
            .send()
            .and_then(Response::json::<AccessTokenResp>)
            .await?
            .try_into()
    }
    async fn get_valid_acc_token(&self) -> Result<Arc<String>, anyhow::Error> {
        let current = Local::now().naive_local();
        // 尝试获取读锁
        let guard = self.wecom_session.read().await;
        if guard.expire_at > current {
            //RAII机制确保了这里读锁的释放
            // Access token 未过期，返回
            return Ok(guard.acc_token.clone());
        }
        //释放读锁
        drop(guard);
        // 尝试获取写锁来更新 access_token
        let mut write_guard = self.wecom_session.write().await;
        // double check再次检查是否已经被其他线程更新
        if write_guard.expire_at > current {
            return Ok(write_guard.acc_token.clone());
        }

        let latest_session = self.try_init_session().await?;
        let latest_acc_token = latest_session.acc_token.clone();
        *write_guard = latest_session; // 更新
        Ok(latest_acc_token)
    }
    pub fn token(&self) -> &str {
        &self.token
    }
    pub fn encoding_aes_key(&self) -> &str {
        &self.encoding_aes_key
    }

    pub fn app_id(&self) -> &str {
        &self.app_id
    }
    pub async fn push_message(&self, to_user: &str, msg: &str) {
        let acc_token = self
            .get_valid_acc_token()
            .await
            .expect("unable to get valid acc token!");
        let req = MsgPushReq {
            touser: to_user,
            msgtype: "text",
            agentid: self.app_agent_id,
            text: HashMap::from([("content", msg)]),
            safe: 0,
        };

        let resp_result = self
            .post(&self.message_push_url)
            .query(&[("access_token", acc_token)])
            .json(&req)
            .send()
            .await;

        let resp = match resp_result {
            Ok(r) => r,
            Err(e) => {
                tracing::error!("push message error: {}", e);
                return;
            }
        };
        let body = resp
            .json::<AccessTokenResp>()
            .await
            .expect("wx json des err");

        match body.errmsg.eq("ok") {
            true => tracing::info!("push message succeed!"),
            false => tracing::error!("push message error!{}", body.errmsg),
        }
    }
}

pub async fn notify_all(messages: &[String], to_users: &[&'static str]) {
    let client = cli();
    let tasks: Vec<_> = messages
        .iter()
        .flat_map(|msg| {
            to_users
                .iter()
                .map(|to_user| client.push_message(to_user, msg))
        })
        .collect();

    join_all(tasks).await;
}
